package teabag;

import java.io.File;
import java.io.IOException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class ScriptManager {
	
	protected static VariableManager vm;
	protected static PropManager pm;
	protected static ActionManager am;
	protected static Math math;
	
	private static ScriptManager instance;
	protected static XPath xpath;
	
	private static Document script;
	
	private ScriptManager()
	{
		xpath = XPathFactory.newInstance().newXPath();
		math = Math.getInstance();
		vm = VariableManager.getInstance();	
		pm = PropManager.getInstance();
		am = ActionManager.getInstance();
		
	}
	
	protected static ScriptManager getInstance()
	{
		if( instance == null )
		{
			instance = new ScriptManager();
		}
		return instance;
	}
	
	protected void setScript( File scriptFile )
	{
		script = parseXmlFile( scriptFile, false );
		//TODO: evaluate script, return success
	}
	
	protected void startGame()
	{
		try {
			Node commandListNode = (Node) xpath.evaluate("root/action[@name=\"default\"]/do", script, XPathConstants.NODE);
			parseDo( commandListNode );
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}
	}
	private void parseDo( Node commandSet )
	{
		parseDo( commandSet, null );
	}
	private void parseDo( Node commandSet, String prop )
	{
		NodeList commands = commandSet.getChildNodes();
		for( int c = 0; c < commands.getLength(); c++ )
		{
			interpretCommand( commands.item(c), prop );
		}
	}
	
	private void interpretCommand( Node command, String prop )
	{
		if( command.getNodeType() == Node.TEXT_NODE )
			return;
		
		Element commandElement = (Element)command;

		String commandName = commandElement.getNodeName();
		Command c = Command.valueOf(commandName.toUpperCase());
		switch (c)
		{
			case ADD:
				add( command );
				break;
			case SUBTRACT:
				subtract( command );
				break;
			case MULTIPLY:
				multiply( command );
				break;
			case DIVIDE:
				divide( command );
				break;
			case ADDADJECTIVE:
				addAdjective( command );
				break;
			case REMOVEADJECTIVE:
				removeAdjective( command );
				break;
			case WITH:
				with( command, prop );
			case REMOVE:
				remove( command );
				break;
			case CREATE:
				create( command );
				break;
			case CALL:
				call( command );
				break;
			case LOG:
				System.out.println( pm );
				break;
			case NEW:
				newVar( command, prop );
				break;
			case SAY:
				say( command );
				break;
			case SET:
				set( command );
				break;
			case IF:
				conditional( command );
				break;
			default:
				System.out.println( "Command '" + commandName + "' is not supported." );
		}

	}
	
	private void add( Node command )
	{
		Element addCommand = (Element)command;
		
		String a = addCommand.getAttribute( "a" );
		String b = addCommand.getAttribute( "b" );
		String into = addCommand.getAttribute( "into" );
		
		math.add(a, b, into);
	}
	
	private void subtract( Node command )
	{
		Element subtractCommand = (Element)command;
		
		String a = subtractCommand.getAttribute( "a" );
		String b = subtractCommand.getAttribute( "b" );
		String into = subtractCommand.getAttribute( "into" );
		
		math.subtract( a, b, into );
	}
	
	private void multiply( Node command )
	{
		Element multiplyCommand = (Element)command;
		
		String a = multiplyCommand.getAttribute( "a" );
		String b = multiplyCommand.getAttribute( "b" );
		String into = multiplyCommand.getAttribute( "into" );
		
		math.multiply( a, b, into );
	}
	
	private void divide( Node command )
	{
		Element divideCommand = (Element)command;
		
		String a = divideCommand.getAttribute( "a" );
		String b = divideCommand.getAttribute( "b" );
		String into = divideCommand.getAttribute( "into" );
		
		math.divide( a, b, into );
	}
	
	private void addAdjective( Node command )
	{
		Element addAdjectiveCommand = (Element)command;
		String prop = addAdjectiveCommand.getAttribute( "prop" );
		String name = addAdjectiveCommand.getAttribute( "name" );
		PropManager.Prop p = pm.getProp( prop );
		if( p != null )
		{
			p.addAdjective( name );
		}
		else
		{
			System.out.println( "No such prop '" + prop + "'" );
		}
	}
	
	private void removeAdjective( Node command )
	{
		Element remAdjectiveCommand = (Element)command;
		String prop = remAdjectiveCommand.getAttribute( "prop" );
		String name = remAdjectiveCommand.getAttribute( "name" );
		PropManager.Prop p = pm.getProp( prop );
		if( p != null )
		{
			p.removeAdjective( name );
		}
		else
		{
			System.out.println( "No such prop '" + prop + "'" );
		}
	}
	
	private void with( Node command, String useProp )
	{
		Element paramCommand = (Element)command;
		String paramProp = paramCommand.getAttribute( "prop" );
		if( paramProp.equals(useProp) ) parseDo( command );
	}
	
	private void remove( Node command )
	{
		Element removeCommand = (Element)command;
		String propName = removeCommand.getAttribute( "name" );
		pm.destroyProp( propName );
	}
	
	private void create( Node command )
	{
		Element createCommand = (Element)command;
		String propName = createCommand.getAttribute("name");
		PropManager.Prop p = pm.createProp( propName );
		if( p == null )
		{
			System.out.println( "The prop '" + propName + "' could not be created (maybe it already exists?)" );
			return;
		}
		try {
			//Run prop constructor
			Node propConstructor = (Node) xpath.evaluate("root//prop[@name=\"" + propName + "\"]/do", script, XPathConstants.NODE );
			parseDo( propConstructor, propName );
			
			//Register prop adjectives
			NodeList adjectives = (NodeList) xpath.evaluate( "root//prop[@name=\"" + propName + "\"]/adjective", script, XPathConstants.NODESET);
			Element adjective;
			for( int a = 0; a < adjectives.getLength(); a++ )
			{
				adjective = (Element)adjectives.item(a);
				p.addAdjective( adjective.getAttribute( "name" ) );
			}
			
			//Register prop actions
			NodeList actions = (NodeList) xpath.evaluate( "root//prop[@name=\"" + propName + "\"]/action", script, XPathConstants.NODESET );
			for( int n = 0; n < actions.getLength(); n++ )
				p.addAction( actions.item(n) );

		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}
	}
	
	private void call( Node command )
	{
		Element callCommand = (Element)command;
		String action = callCommand.getAttribute( "name" );
		String prop = callCommand.getAttribute( "use" );
		Node doNode = am.getCommandNode(action);
		if( doNode != null )
		{
			System.out.println("[ " + action + ( (prop.length() > 0) ? " with " + prop + " ]" : " ]" ));
			parseDo( doNode, prop );
		}
		else
		{
			System.out.println( "Action '" + action + "' does not exist" );
		}
	}
	
	private void conditional( Node command )
	{
		Element ifCommand = (Element)command;
		String condition = ifCommand.getAttribute("condition");
		if( !vm.exists(condition) )
		{
			System.out.println( "Variable '" + condition + "' does not exist");
			return;
		}
		if( Type.BOOLEAN != vm.getType( condition ) )
		{
			System.out.println( "Type mismatch in '" + condition + "': Expecting BOOLEAN, Found " + vm.getType( condition ) );
			return;
		}
		
		String value = vm.getValue( condition );
		NodeList commandList = ifCommand.getChildNodes(); 
		for( int n = 0; n < commandList.getLength(); n++ )
		{
			if( commandList.item(n).getNodeName().equals( value.toLowerCase() ) )
			{
				parseDo( commandList.item(n) );
				return;
			}
		}
	}
	
	private void newVar( Node command, String prop )
	{
		Element newCommand = (Element)command;
		String name = newCommand.getAttribute("name");
		String value = newCommand.getAttribute("value");
		PropManager.Prop p = pm.getProp( prop );
		if( p == null )
		{
			vm.addVariable(name, value);
		}
		else
		{
			p.newVariable(name, value);
		}
	}
	
	private void set( Node command )
	{
		Element setCommand = (Element)command;
		String name = setCommand.getAttribute("name");
		String value = setCommand.getAttribute("value");
		vm.setValue(name, value);
	}
	
	private void say( Node command )
	{
		Element sayCommand = (Element)command;
		String message = sayCommand.getAttribute("message");
		System.out.println( message );
	}
	
    private Document parseXmlFile(File xmlFile, boolean validating) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setValidating(validating);
            Document doc = factory.newDocumentBuilder().parse(xmlFile);
            return doc;
        } catch (SAXException e) {
        	System.out.println( "Invalid XML in file " + xmlFile.getName() );
        	e.printStackTrace();
        } catch (ParserConfigurationException e) {
        	e.printStackTrace();
        } catch (IOException e) {
        	e.printStackTrace();
        }
        return null;
    }
    
    private enum Command
    {
    	ADD, SUBTRACT, MULTIPLY, DIVIDE, ADDADJECTIVE, REMOVEADJECTIVE, WITH, REMOVE, CREATE, CALL, LOG, NEW, SAY, SET, IF;
    }
}
